//
//  APIsTagsHandler.swift
//  MacDemo
//
//  Created by FH on 2020/12/8.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

enum AstNodeTagAvailable : String, RawRepresentable, Codable {
    case available = ""
    case deprecated = "deprecated"
}

enum AstNodeTagPlatform : String, RawRepresentable, Codable {
    case common = ""
    case macOnly = "MacOnly"
    case iOSOnly = "iOSOnly"
}

enum AstNodeTagOrder : String, RawRepresentable, Codable {
    case common = ""
    case before = "before"
    case after = "after"
    case leave = "leave"
    
    init?(caseIndex: Int?) {
        guard let index = caseIndex else {
            return nil
        }
        if index == 4 {
            self = .common
        } else if index == 3 {
            self = .leave
        } else if index == 2 {
            self = .after
        } else if index == 1 {
            self = .before
        } else {
            self.init(rawValue: "")
        }
    }
    
    static func toList() -> [String] {
        return ["all", "before", "after", "leave", "common"]
    }
}

enum AstNodeTagScene : String, RawRepresentable, Codable {
    case main = ""
    case setting = "setting"
    case common = "common"
    
    init?(caseIndex: Int?) {
        guard let index = caseIndex else {
            return nil
        }
        if index == 3 {
            self = .main
        } else if index == 2 {
            self = .common
        } else if index == 1 {
            self = .setting
        } else {
            self.init(rawValue: "")
        }
    }
    
    static func toList() -> [String] {
        return ["all", "setting", "common", "main"]
    }
}

enum AstNodeTagType : String, RawRepresentable, Codable {
    case available = "available"
    case grouping = "grouping"
    case callAPIOrder = "order"
    case callAPIScene = "scene"
    case callAPILimit = "call"
    case platformLimit = "platform"
    
    init?(caseIndex: Int?) {
        guard let index = caseIndex else {
            return nil
        }
        if index == 2 {
            self = .callAPIScene
        } else if index == 1 {
            self = .callAPIOrder
        } else {
            self = .grouping
        }
    }
}

struct TagOrderConfig {
    let orderBy: AstNodeTagType
    let exclude = [AstNodeTagType]()
    let filterByTags = [String]()
    let bottomByTag = ""
}

extension AstNode {
    func addTag<TagType: RawRepresentable>(type: AstNodeTagType, tag: TagType) where TagType.RawValue == String {
        addTag(type: type.rawValue, tag: tag.rawValue)
    }
    
    func addTag(type: AstNodeTagType, tag: String) {
        addTag(type: type.rawValue, tag: tag)
    }
    
    func addTag(type: String, tag: String) {
        self.tags[type] = tag
    }
    
    func removeTag(byType: String) {
        self.tags.removeValue(forKey: byType)
    }
    
    func removeTag(byValue: String) {
        if byValue.count > 0, let index = self.tags.firstIndex(where: { $0.value == byValue }) {
            self.tags.remove(at: index)
        }
    }
    
    func containsTag(_ type: AstNodeTagType) -> Bool {
        return containsTag(type.rawValue)
    }
    
    func containsTag(_ type: String) -> Bool {
        return self.tags[type] != nil
    }
}

class APIsTagsHandler {
    private let regex = try! Regex(pattern: "#pragma\\s+mark\\s+(-\\s)?(.+?)\\n", groupNames: "symbol", "name")
    // TODO: use APIsType enum
    private let apiClassName: String
    private let delegateClassName: String
    private var headerReader: LineReader!
    private var astNodes: [String : AstNode]
    // cached tags for order
    fileprivate var cachedGroupNodes = [String : [String : [AstNode]]]()
    fileprivate var cachedGroupNames = [String : [String]]()
    fileprivate var cachedOrderNodes = [String : [String : [AstNode]]]()
    fileprivate var cachedSceneNodes = [String : [String : [AstNode]]]()
    // cached for search
    fileprivate var cachedAllNodes = [String : AstNode]()
    
    init(astNodes: [String : AstNode], objcAPIsHeaderPath: String?, apiClassName: String, delegateClassName: String) {
        self.astNodes = astNodes
        self.apiClassName = apiClassName
        self.delegateClassName = delegateClassName
        self.headerReader = LineReader(path: objcAPIsHeaderPath!)
    }
    
    func parse() -> [String : AstNode] {
        var loadFromCache = true
        // parse group tag
        if let firstNode = astNodes[self.apiClassName]?.children.first, firstNode.tags.count == 0 {
            loadFromCache = false
        }
        var processedMethods = Set<AstNode>()
        var rootNode: AstNode!
        var cachedTagKey: String!
        var currGroup: String!
        var isDeprecated = false
        for line in headerReader {
            let currLine = line.trimmingCharacters(in: .whitespaces)
            if line.starts(with: "#pragma mark") {
                let match = self.regex.findFirst(in: line)!
                var remarkName = match.group(named: "name")!
                if remarkName == "Delegate Methods" {
                    cachedTagKey = delegateClassName
                    cachedGroupNodes[delegateClassName] = [:]
                    cachedGroupNames[delegateClassName] = []
                    rootNode = astNodes[self.delegateClassName]!
                } else if remarkName == "AgoraRtcEngineKit" {
                    cachedTagKey = apiClassName
                    cachedGroupNodes[apiClassName] = [:]
                    cachedGroupNames[apiClassName] = []
                    rootNode = astNodes[self.apiClassName]!
                } else {
                    if remarkName.contains("Deprecated") {
                        isDeprecated = true
                    }
                    currGroup = remarkName
                    isDeprecated = false
                    if let rootNode = rootNode {
                        remarkName = remarkName.replacingOccurrences(of: "Delegate ", with: "")
                        // save group order
                        if rootNode.type == .protocol {
                            self.cachedGroupNames[cachedTagKey]!.append(remarkName)
                        } else if rootNode.type == .interface {
                            self.cachedGroupNames[cachedTagKey]!.append(remarkName)
                        }
                    }
                }
            } else if currLine.starts(with: "+ (")
                || currLine.starts(with: "- (")
                || currLine.starts(with: "-(")
                || currLine.starts(with: "+(") {
                for method in rootNode.children where method.type == .method {
                    if processedMethods.contains(method) {
                        continue
                    }
                    let split = method.splitSelector()
                    var isMatch = false
                    for section in split {
                        if section == "" {
                            continue
                        }
                        if line.contains(section) {
                            isMatch = true
                        }
                    }
                    if isMatch {
                        if isDeprecated {
                            if !method.isDeprecated {
                                method.isDeprecated = true
                                addTag(loadFromCache, method, .available, AstNodeTagAvailable.deprecated)
                                print(">>> not handle \(method.name) deprecated ...")
                            }
                        }
                        // add tag
                        if !loadFromCache {
                            method.addTag(type: .grouping, tag: currGroup!)
                        }
                        processedMethods.insert(method)
                        // cache order
                        if var groupNodes = self.cachedGroupNodes[cachedTagKey] {
                            var nodes = groupNodes[currGroup!] ?? []
                            nodes.append(method)
                            groupNodes[currGroup!] = nodes
                            self.cachedGroupNodes[cachedTagKey] = groupNodes                            
                            break
                        }
                    }
                }
            }
        }

        // manual to handle other tags
        // for AstNodeTagOrder
        let before = ["sharedEngineWithAppId:delegate:",
                      "sharedEngineWithConfig:delegate:",
                      "sharedEngineWithAppId:error:",
                      "startEchoTestWithInterval:successBlock:",
                      "joinChannelByToken:channelId:info:uid:joinSuccess:",
                      "joinChannelByToken:channelId:info:uid:options:",
                      "joinChannelByUserAccount:token:channelId:joinSuccess:",
                      "joinChannelByUserAccount:token:channelId:options:",
                      "stopEchoTest",
                      "setChannelProfile:",
                      "setClientRole:",
                      "setClientRole:options:",
                      "registerLocalUserAccount:appId:",
                      "setAudioProfile:scenario:",
                      "setDefaultAudioRouteToSpeakerphone:",
                      "enableExternalAudioSourceWithSampleRate:channelsPerFrame:",
                      "enableSoundPositionIndication:",
                      "preloadEffect:filePath:",
                      "createRtcChannel:",
                      "setRecordingAudioFrameParametersWithSampleRate:channel:mode:samplesPerCall:",
                      "setPlaybackAudioFrameParametersWithSampleRate:channel:mode:samplesPerCall:",
                      "setRemoteUserPriority:type:",
                      "setLocalPublishFallbackOption:",
                      "setRemoteSubscribeFallbackOption:",
                      "enableEncryption:encryptionConfig:",
                      "setCameraCapturerConfiguration:",
                      "setMediaMetadataDataSource:withType:",
                      "setMediaMetadataDelegate:withType:",
                      "startRecordingDeviceTest:",
                      "stopRecordingDeviceTest",
                      "startPlaybackDeviceTest:",
                      "stopPlaybackDeviceTest",
                      "startCaptureDeviceTest:",
                      "stopCaptureDeviceTest",
                      "startAudioDeviceLoopbackTest:",
                      "stopAudioDeviceLoopbackTest",
                      "setLogFile",
                      "setLogFileSize:"]
        let leave = ["rate:rating:description:",
                     "complain:description:",
                     "destroy"]
        let after = ["getUserInfoByUserAccount:withError:",
                     "getUserInfoByUid:withError:"]
        

        // for AstNodeTagPlatform
        let iOSOnly = ["setEnableSpeakerphone:",
                       "setDefaultAudioRouteToSpeakerphone:",
                       "isSpeakerphoneEnabled",
                       "enableInEarMonitoring:",
                       "setInEarMonitoringVolume:",
                       "setAudioSessionOperationRestriction:",
                       "switchCamera",
                       "isCameraZoomSupported",
                       "isCameraTorchSupported",
                       "isCameraFocusPositionInPreviewSupported",
                       "isCameraExposurePositionSupported",
                       "isCameraAutoFocusFaceModeSupported",
                       "setCameraZoomFactor:",
                       "setCameraFocusPositionInPreview:",
                       "setCameraExposurePosition:",
                       "setCameraTorchOn:",
                       "setCameraAutoFocusFaceModeEnabled:"]
        let MacOnly = ["monitorDeviceChange:",
                       "enumerateDevices:",
                       "getDeviceInfo:",
                       "setDevice:deviceId:",
                       "getDeviceVolume:",
                       "setDeviceVolume:volume:",
                       "startRecordingDeviceTest:",
                       "stopRecordingDeviceTest",
                       "startPlaybackDeviceTest:",
                       "stopPlaybackDeviceTest",
                       "startCaptureDeviceTest:",
                       "stopCaptureDeviceTest",
                       "startAudioDeviceLoopbackTest:",
                       "stopAudioDeviceLoopbackTest",
                       "startScreenCaptureByDisplayId:rectangle:parameters:",
                       "startScreenCaptureByWindowId:rectangle:parameters:",
                       "updateScreenCaptureParameters:",
                       "updateScreenCaptureRegion:",
                       "stopScreenCapture",
                       "enableLoopbackRecording:deviceName:",
                       "setScreenCaptureContentHint:"]
        
        // for AstNodeTagScene
        let common: Set<String> = ["monitorDeviceChange:",
                                   "enumerateDevices:",
                                   "getDeviceInfo:",
                                   "setDevice:deviceId:",
                                   "getDeviceVolume:",
                                   "setDeviceVolume:volume:",
                                   "startRecordingDeviceTest:",
                                   "stopRecordingDeviceTest",
                                   "startPlaybackDeviceTest:",
                                   "stopPlaybackDeviceTest",
                                   "startCaptureDeviceTest:",
                                   "stopCaptureDeviceTest",
                                   "startAudioDeviceLoopbackTest:",
                                   "stopAudioDeviceLoopbackTest",
                                   "getSdkVersion",
                                   "getErrorDescription:",
                                   "sendCustomReportMessage:category:event:label:value:",
                                   "getNativeHandle",
                                   "getUserInfoByUserAccount:withError:",
                                   "getUserInfoByUid:withError:",
                                   "getConnectionState",
                                   "startEchoTestWithInterval:successBlock:",
                                   "stopEchoTest",
                                   "enableLastmileTest",
                                   "disableLastmileTest",
                                   "startLastmileProbeTest:",
                                   "stopLastmileProbeTest"]
        let setting = ["setLogFilter:",
                       "enableAudio",
                       "disableAudio",
                       "adjustRecordingSignalVolume:",
                       "adjustPlaybackSignalVolume:",
                       "enableAudioVolumeIndication:smooth:report_vad:",
                       "enableLocalAudio:",
                       "muteLocalAudioStream:",
                       "muteAllRemoteAudioStreams:",
                       "setDefaultMuteAllRemoteAudioStreams:",
                       "enableVideo",
                       "disableVideo",
                       "setVideoEncoderConfiguration:",
                       "enableLocalVideo:",
                       "muteLocalVideoStream:",
                       "muteAllRemoteVideoStreams:",
                       "muteRemoteVideoStream:mute:",
                       "setDefaultMuteAllRemoteVideoStreams:",
                       "setBeautyEffectOptions:options:",
                       "enableFaceDetection:",
                       "isSpeakerphoneEnabled",
                       "enableInEarMonitoring:",
                       "setInEarMonitoringVolume:",
                       "setLocalVoicePitch:",
                       "setLocalVoiceEqualizationOfBandFrequency:withGain:",
                       "setLocalVoiceReverbOfType:withValue:",
                       "setVoiceBeautifierPreset:",
                       "setAudioEffectPreset:",
                       "setAudioEffectParameters:param1:param2:",
                       "enableLoopbackRecording:deviceName:",
                       "setAudioSessionOperationRestriction:",
                       "setVideoSource:",
                       "setLocalVideoRenderer:",
                       "enableExternalAudioSink:channels:",
                       "setMixedAudioFrameParametersWithSampleRate:samplesPerCall:",
                       "addVideoWatermark:options:",
                       "clearVideoWatermarks",
                       "enableDualStreamMode:",
                       "switchCamera",
                       "isCameraZoomSupported",
                       "isCameraTorchSupported",
                       "isCameraFocusPositionInPreviewSupported",
                       "isCameraExposurePositionSupported",
                       "isCameraAutoFocusFaceModeSupported",
                       "setCameraZoomFactor:",
                       "setCameraFocusPositionInPreview:",
                       "setCameraExposurePosition:",
                       "setCameraTorchOn:",
                       "setCameraAutoFocusFaceModeEnabled:",
                       "setScreenCaptureContentHint:",
                       "enableMainQueueDispatch:",
                       "muteRemoteAudioStream:mute:",
                       "setupLocalVideo:",
                       "setupRemoteVideo:",
                       "setLocalRenderMode:mirrorMode:",
                       "setRemoteRenderMode:renderMode:mirrorMode:",
                       "startPreview",
                       "setRemoteVideoRenderer:forUserId:",
                       "setExternalVideoSource:useTexture:pushMode:",
                       "pushExternalVideoFrame:"]
        
        for method in astNodes[self.apiClassName]!.children where method.type == .method {
            if before.contains(method.name) {
                addTag(loadFromCache, method, .callAPIOrder, AstNodeTagOrder.before)
                if !common.contains(method.name) {
                    addTag(loadFromCache, method, .callAPIScene, AstNodeTagScene.setting)
                }
            } else if leave.contains(method.name) {
                addTag(loadFromCache, method, .callAPIOrder, AstNodeTagOrder.leave)
                addTag(loadFromCache, method, .callAPIScene, AstNodeTagScene.main)
            }
                
            if common.contains(method.name) {
                addTag(loadFromCache, method, .callAPIScene, AstNodeTagScene.common)
            } else if setting.contains(method.name) {
                addTag(loadFromCache, method, .callAPIScene, AstNodeTagScene.setting)
            }
            
            if after.contains(method.name)
                || !(before.contains(method.name)
                    || common.contains(method.name)
                    || setting.contains(method.name)
                    || leave.contains(method.name)) {
                addTag(loadFromCache, method, .callAPIOrder, AstNodeTagOrder.after)
                if !common.contains(method.name) {
                    addTag(loadFromCache, method, .callAPIScene, AstNodeTagScene.main)
                }
            } else if !(before.contains(method.name) || leave.contains(method.name)) {
                addTag(loadFromCache, method, .callAPIOrder, AstNodeTagOrder.common)
            }
            
            if MacOnly.contains(method.name) {
                addTag(loadFromCache, method, .platformLimit, AstNodeTagPlatform.macOnly)
            } else if iOSOnly.contains(method.name) {
                addTag(loadFromCache, method, .platformLimit, AstNodeTagPlatform.iOSOnly)
            } else {
                addTag(loadFromCache, method, .platformLimit, AstNodeTagPlatform.common)
            }
        }
        // TODO: remove
        let nodes = self.astNodes
        self.astNodes.removeAll()
        self.headerReader = nil
        self.cachedAllNodes = self.getAllNodes(nodes)
        return nodes
    }
    
    private func addTag<TagType: RawRepresentable>(_ loadFromCache: Bool,
                                                   _ method: AstNode,
                                                   _ type: AstNodeTagType,
                                                   _ tag: TagType)
        where TagType.RawValue == String {
        if loadFromCache {
            let forOrder = type == .callAPIOrder
            let forScene = type == .callAPIScene
            var nodes: [String : [AstNode]]! = nil
            if forOrder {
                nodes = self.cachedOrderNodes[apiClassName] ?? [:]
            } else if forScene  {
                nodes = self.cachedSceneNodes[apiClassName] ?? [:]
            }
            if forOrder || forScene {
                var tagKey = tag.rawValue
                if tagKey.count == 0 {
                    if forOrder {
                        tagKey = "all"
                    } else if forScene {
                        tagKey = "main"
                    }
                }
                var items = nodes[tagKey] ?? []
                items.append(method)
                nodes[tagKey] = items
                if forOrder {
                    self.cachedOrderNodes[apiClassName] = nodes
                } else if forScene {
                    self.cachedSceneNodes[apiClassName] = nodes
                }
            }
        } else {
            method.addTag(type: type, tag: tag)
        }
    }
    
    fileprivate func getAllNodes(_ allNodes: [String : AstNode]) -> [String : AstNode] {
        var nodes = [String : AstNode].init(minimumCapacity: 256)
         for (_, node) in allNodes {
             self.getSubNode(node, nodes: &nodes)
         }
        return nodes
    }
    
    fileprivate func getSubNode(_ node: AstNode, nodes: inout [String : AstNode]) {
        nodes[node.name] = node
        if node.type == .interface || node.type == .protocol {
            for subNode in node.children {
                getSubNode(subNode, nodes: &nodes)
            }
        }
    }
}

// REMARK: for sort fuzzy result
fileprivate class FuzzyResultSort {
    class Item : CustomStringConvertible {
        let node: AstNode
        var isExists = false
        var description: String {
            return "\(node.name) isExists == \(self.isExists)"
        }
        
        init(_ node: AstNode, _ isExists: Bool) {
            self.node = node
            self.isExists = isExists
        }
    }
    
    var itemNames = Set<String>()
    var sortedItems = [Item]()
    
    func add(_ node: AstNode, parent: AstNode?) {
        let currNode = parent ?? node
        if itemNames.contains(currNode.name) {
            if parent == nil {
                for item in self.sortedItems where !item.isExists {
                    if item.node.name == currNode.name {
                        item.isExists = true
                        break
                    }
                }
            }
        } else {
            itemNames.insert(currNode.name)
            sortedItems.append(Item(currNode, parent == nil))
            if sortedItems.count > 1 {
                sortedItems.sort { (item1, item2) in
                    let value = (item1.node.type, item2.node.type)
                    switch value {
                    case (let lhs, let rhs) where lhs == rhs:
                        return item1.node.name > item2.node.name
                    case (.interface, _):
                        return true
                    case (_, .interface):
                        return false
                    case (.enumerate, _):
                        return true
                    case (_, .enumerate):
                        return false
                    default:
                        return false
                    }
                }
            }
        }
    }
    
    func sort(nodes: [AstNode]) -> [AstNode] {
        var result = [AstNode]()
        // add main-type nodes first
        for item in self.sortedItems where item.isExists {
            result.append(item.node)
        }
        for item in self.sortedItems {
            var methods = [AstNode]()
            var properties = [AstNode]()
            for node in nodes {
                if node.parentName == item.node.name {
                    if node.type == .method {
                        methods.append(node)
                    } else if node.type == .property {
                        methods.append(node)
                    }
                }
            }
            methods.sort { (item1, item2) in
                return item1.order < item2.order
            }
            properties.sort { (item1, item2) in
                return item1.order < item2.order
            }
            if methods.count > 0 {
                result.append(contentsOf: methods)
            }
            if properties.count > 0 {
                result.append(contentsOf: properties)
            }
        }
        return result
    }
}

// extention for get tags
extension APIsTagsHandler {
    func getNodeTagTypes(apisType: String) -> [String] {
        if apisType == self.apiClassName {
            return ["by \(AstNodeTagType.grouping.rawValue)",
                    "by \(AstNodeTagType.callAPIOrder.rawValue)",
                    "by \(AstNodeTagType.callAPIScene.rawValue)"]
        } else if apisType == self.delegateClassName {
            return ["by \(AstNodeTagType.grouping.rawValue)"]
        } else {
            return []
        }
    }
    
    func getGroupTags(apisEntryType: String) -> [String]? {
        if let names = self.cachedGroupNames[apisEntryType] {
            return ["all"] + names
        }
        return nil
    }
    
    func searchAPIsNodes(apisEntryType: String, tagTypeCaseIndex: Int?, caseIndex: Int?) -> [AstNode] {
        var nodes = [AstNode]()
        if let tagType = AstNodeTagType(caseIndex: tagTypeCaseIndex) {
            if tagType == .grouping {
                if let groupNames = self.cachedGroupNames[apisEntryType] {
                    var filterName: String! = nil
                    for index in 0..<groupNames.count {
                        if (index+1) == caseIndex {
                            filterName = groupNames[index]
                            break
                        }
                    }
                    for name in groupNames {
                        if let groupNodes = self.cachedGroupNodes[apisEntryType] {
                            if filterName != nil && filterName != name {
                                continue
                            }
                            if let group = groupNodes[name] {
                                nodes.append(contentsOf: group)
                            }
                        }
                    }
                }
            } else if tagType == .callAPIOrder {
                if let orderNodes = self.cachedOrderNodes[apisEntryType] {
                    if let order = AstNodeTagOrder(caseIndex: caseIndex) {
                        if let findNodes = orderNodes[order.rawValue] {
                            nodes.append(contentsOf: findNodes)
                        }
                    } else {
                        for name in AstNodeTagOrder.toList() {
                            if name != "all" {
                                if let findNodes = orderNodes[name] {
                                    nodes.append(contentsOf: findNodes)
                                }
                            }
                        }
                    }
                }
            } else if tagType == .callAPIScene {
                if let sceneNodes = self.cachedSceneNodes[apisEntryType] {
                    if let scene = AstNodeTagScene(caseIndex: caseIndex) {
                        if let findNodes = sceneNodes[scene.rawValue] {
                            nodes.append(contentsOf: findNodes)
                        }
                    } else {
                        for name in AstNodeTagScene.toList() {
                            if name != "all" {
                                if let findNodes = sceneNodes[name] {
                                    nodes.append(contentsOf: findNodes)
                                }
                            }
                        }
                    }
                }
            }
        } else {
            print("TODO: remove")
//            if let node = allNodes[apisEntryType], (node.type == .interface || node.type == .protocol) {
//                nodes.append(contentsOf: node.children)
//            }
        }
        return nodes
    }

    func fuzzySearchAPIsName(_ needle: String?, _ apisTypeCaseIndex: Int?) -> [AstNode] {
        let result = FuzzyResultSort()
        var nodes = [AstNode]()
        for (name, node) in self.cachedAllNodes {
            // filter by nodeType
            if let caseIndex = apisTypeCaseIndex, let type = AstNodeType(enumCase: caseIndex),
                node.type != type {
                continue
            }
            // filter by keyword
            if let needle = needle {
                if !name.fuzzyMatch(needle) {
                    continue
                }
            }
            
            var parent: AstNode? = nil
            if !node.isMainNode {
                parent = self.cachedAllNodes[node.parentName!]
                result.add(node, parent: parent)
            }
            nodes.append(node)
        }

        return result.sort(nodes: nodes)
    }
}
